# Copyright (c) 2023 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import re
from dataclasses import dataclass
from typing import Optional
from xml.sax.xmlreader import AttributesImpl

from matter.idl.generators.type_definitions import GetDataTypeSizeInBits, IsSignedDataType
from matter.idl.matter_idl_types import AccessPrivilege, Attribute, Command, ConstantEntry, DataType, Event, EventPriority, Field

log = logging.getLogger(__name__)


@dataclass
class ParsedType:
    name: str
    is_list: bool = False


def ParseInt(value: str, data_type: Optional[DataType] = None) -> int:
    """
    Convert a string that is a known integer into an actual number.

    Supports decimal or hex values prefixed with '0x'
    """
    if value.startswith('0x'):
        parsed = int(value[2:], 16)
        if data_type and IsSignedDataType(data_type):
            bits = GetDataTypeSizeInBits(data_type)
            assert (bits)  # size MUST be known
            if parsed & (1 << (bits - 1)):
                parsed -= 1 << bits
        return parsed
    return int(value)


def ParseOptionalInt(value: str) -> Optional[int]:
    """Parses numbers as long as they are in an expected format of numbers.

       "1" parses to 1
       "0x12" parses to 18
       "Min" parses to None
    """
    if re.match("^-?((0x[0-9a-fA-F]*)|([0-9]*))$", value):
        return ParseInt(value)

    return None


_TYPE_REMAP = {
    # unsigned
    "uint8": "int8u",
    "uint16": "int16u",
    "uint24": "int24u",
    "uint32": "int32u",
    "uint48": "int48u",
    "uint52": "int52u",
    "uint64": "int64u",
    # signed (map to what zapxml/matter currently has)
    "int8": "int8s",
    "int16": "int16s",
    "int24": "int24s",
    "int32": "int32s",
    "int48": "int48s",
    "int52": "int52s",
    "int64": "int64s",
    # other
    "bool": "boolean",
    "string": "char_string",
    "octets": "octet_string",
}


def NormalizeDataType(t: str) -> str:
    """Convert data model xml types into matter idl types."""
    return _TYPE_REMAP.get(t.lower(), t.replace("-", "_"))


# Handle oddities in current data model XML schema for nicer diffs
_REF_NAME_MAPPING = {
    "<<ref_DataTypeEndpointNumber>>": "endpoint_no",
    "<<ref_DataTypeEpochUs>>": "epoch_us",
    "<<ref_DataTypeNodeId>>": "node_id",
    "<<ref_DataTypeOctstr>>": "octet_string",
    "<<ref_DataTypeString>>": "char_string",
    "<<ref_DataTypeVendorId>>": "vendor_id",
    "<<ref_FabricIdx>>": "fabric_idx",
}


# Handle odd casing and naming
_CASE_RENAMES_MAPPING = {
    "amperage_mA": "amperage_ma",
    "power_mW": "power_mw",
    "power_mVA": "power_mva",
    "power_mVAR": "power_mvar",
    "energy_mWh": "energy_mwh",
    "energy_mVAh": "energy_mvah",
    "energy_mVARh": "energy_mvarh",
    "voltage_mV": "voltage_mv",
}


def ParseType(t: str) -> ParsedType:
    """Parse a data type entry.

    Specifically parses a name like "list[Foo Type]".
    """

    # very rough matcher ...
    is_list = False
    if t.startswith("list[") and t.endswith("]"):
        is_list = True
        t = t[5:-1]
    elif t.startswith("<<ref_DataTypeList>>[") and t.endswith("]"):
        is_list = True
        t = t[21:-1]

    if t.endswith(" Type"):
        t = t[:-5]

    if t in _REF_NAME_MAPPING:
        t = _REF_NAME_MAPPING[t]

    if t in _CASE_RENAMES_MAPPING:
        t = _CASE_RENAMES_MAPPING[t]

    return ParsedType(name=NormalizeDataType(t), is_list=is_list)


def NormalizeName(name: str) -> str:
    """Convert a free form name from the spec into a programming language
       name that is appropriate for matter IDL.
    """

    # Trim human name separators
    for separator in " /-":
        name = name.replace(separator, '_')
    while '__' in name:
        name = name.replace('__', '_')

    # NOTE: zapt generators for IDL files use a construct of the form
    #       `{{asUpperCamelCase name preserveAcronyms=true}}`
    #       and it is somewhat unclear what preserveAcronyms will do.
    #
    #      Current assumption is that spec already has acronyms set in
    #      the correct place and at least for some basic tests this method
    #      generates good names
    #
    #      If any acronyms seem off in naming at some point, more logic may
    #      be needed here.

    # At this point, we remove all _ and make sure _ is followed by an uppercase
    while name.endswith('_'):
        name = name[:-1]

    while '_' in name:
        idx = name.find('_')
        name = name[:idx] + name[idx + 1].upper() + name[idx + 2:]

    return name


def FieldName(input_name: str) -> str:
    """Normalized name with the first letter lowercase. """
    name = NormalizeName(input_name)

    # Some exception handling for nicer diffs
    if name == "ID":
        return "id"

    # If the name starts with a all-uppercase thing, keep it that
    # way. This is typical for "NOC", "IPK", "CSR" and such
    if len(input_name) > 1:
        if input_name[0].isupper() and input_name[1].isupper():
            return name

    return name[0].lower() + name[1:]


def AttributesToField(attrs: AttributesImpl) -> Field:
    assert "name" in attrs
    assert "id" in attrs

    if "type" in attrs:
        attr_type = NormalizeDataType(attrs["type"])
    else:
        # TODO: Generally we should not have this, however current implementation
        #       for derived clusters for example want to add things (like conformance
        #       specifically) WITHOUT re-stating things like types
        #
        # https://github.com/csa-data-model/projects/issues/365
        log.error("Attribute '%s' has no type", attrs['name'])
        attr_type = "sint32"
    t = ParseType(attr_type)

    return Field(
        name=FieldName(attrs["name"]),
        code=ParseInt(attrs["id"]),
        is_list=t.is_list,
        data_type=DataType(name=t.name),
    )


def AttributesToBitFieldConstantEntry(attrs: AttributesImpl) -> ConstantEntry:
    """Creates a constant entry appropriate for bitmaps.
    """
    assert "name" in attrs

    if 'bit' not in attrs:
        # TODO: multi-bit fields not supported in XML currently. Be lenient here to have some
        #       diff
        # Issue: https://github.com/csa-data-model/projects/issues/347

        log.error("Constant '%s' has no bit value (may be multibit)", attrs['name'])
        return ConstantEntry(name="k" + NormalizeName(attrs["name"]), code=0)

    assert "bit" in attrs

    return ConstantEntry(name="k" + NormalizeName(attrs["name"]), code=1 << ParseInt(attrs["bit"]))


def AttributesToAttribute(attrs: AttributesImpl) -> Attribute:
    assert "name" in attrs
    assert "id" in attrs

    if "type" in attrs:
        attr_type = NormalizeDataType(attrs["type"])
    else:
        # TODO: we should NOT have this, however we are now lenient
        # to bad input data
        log.error("Attribute '%s' has no type", attrs['name'])
        attr_type = "sint32"

    t = ParseType(attr_type)

    return Attribute(
        definition=Field(
            code=ParseInt(attrs["id"]),
            name=FieldName(attrs["name"]),
            is_list=t.is_list,
            data_type=DataType(name=t.name),
        )
    )


def AttributesToEvent(attrs: AttributesImpl) -> Event:
    assert "name" in attrs
    assert "id" in attrs
    if "priority" in attrs:
        if attrs["priority"] == "critical":
            priority = EventPriority.CRITICAL
        elif attrs["priority"] == "info":
            priority = EventPriority.INFO
        elif attrs["priority"] == "debug":
            priority = EventPriority.DEBUG
        elif attrs["priority"] == "desc":
            log.warning("Found an event with 'desc' priority: '%s'", attrs.items())
            priority = EventPriority.CRITICAL
        else:
            raise Exception("UNKNOWN event priority: %r" % attrs["priority"])
    else:
        priority = EventPriority.INFO

    return Event(
        name=NormalizeName(attrs["name"]),
        code=ParseInt(attrs["id"]),
        priority=priority,
        fields=[])


def StringToAccessPrivilege(value: str) -> AccessPrivilege:
    if value == "view":
        return AccessPrivilege.VIEW
    if value == "operate":
        return AccessPrivilege.OPERATE
    if value == "manage":
        return AccessPrivilege.MANAGE
    if value == "admin":
        return AccessPrivilege.ADMINISTER
    raise Exception("UNKNOWN privilege level: %r" % value)


def AttributesToCommand(attrs: AttributesImpl) -> Command:
    assert "id" in attrs
    assert "name" in attrs

    if "response" not in attrs:
        log.warning("Command '%s' has no response set.", attrs['name'])
        # Matter IDL has no concept of "no response sent"
        # Example is DoorLock::"Operating Event Notification"
        #
        # However that is not in the impl in general
        # it is unclear what to do here (and what "NOT" is as conformance)

        output_param = "DefaultSuccess"
    else:
        output_param = NormalizeName(attrs["response"])
        if output_param == "Y":
            output_param = "DefaultSuccess"  # IDL name for no specific struct

    return Command(
        name=NormalizeName(attrs["name"]),
        code=ParseInt(attrs["id"]),
        input_param=None,  # not specified YET
        output_param=output_param
    )
